package edu.hawaii.myisern.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import edu.hawaii.myisern.collaborations.jaxb.Collaboration;
import edu.hawaii.myisern.collaborations.jaxb.Collaborations;
import edu.hawaii.myisern.organizations.jaxb.Organization;
import edu.hawaii.myisern.organizations.jaxb.Organizations;
import edu.hawaii.myisern.researchers.jaxb.Researcher;
import edu.hawaii.myisern.researchers.jaxb.Researchers;

/**
 * A Java class presentation of the ISERN social network. This class converts the XML files to the
 * Java classes instances.
 * 
 * @author Ed Kim
 * @author Jon Lao
 * @author Jianfei Liao
 * @author Jared Sunouchi
 */
public class MyIsernModel {
  /** The singleton MyIsernModel instance. */
  private static MyIsernModel theInstance = new MyIsernModel();

  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext collaborationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext organizationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext researchersJaxbContext;

  /** Holds collaborations data. */
  private Collaborations collaborations;
  /** Holds organizations data. */
  private Organizations organizations;
  /** Holds researchers data. */
  private Researchers researchers;
  /** A String represents "user.home". */
  private static final String USER_HOME = "user.home";
  /** Holds the derby database with the researchers. */
  private DerbyPersistentStore db = new DerbyPersistentStore();

  /**
   * Initializes this instance.
   */
  private MyIsernModel() {
    try {
      // try to read data from the database
      System.out.println("Reading data from the database tables.");
      this.researchers = this.db.getResearchers();
      this.organizations = this.db.getOrganizations();
      this.collaborations = this.db.getCollaborations();
      
      // if the database tables do not exist, import the XML files
      String directory = System.getProperty(USER_HOME);
      if (this.researchers == null || 
          this.organizations == null ||
          this.collaborations == null) {
        System.out.println("Database tables do not exist.");
        this.importResearchersXml(directory + "/.myisern/researchers.black.xml");
        this.importOrganizationsXml(directory + "/.myisern/organizations.black.xml");
        this.importCollaborationsXml(directory + "/.myisern/collaborations.black.xml");

        // create the database tables
        this.createTables();
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  /**
   * Creates the database tables.
   * 
   * @return true if the tables are created, false otherwise.
   */
  protected boolean createTables() {
    boolean isWritten;
    // create the database tables
    System.out.println("Writing database tables.");
    try {
      this.db.writeResearchers(this.researchers);
      this.db.writeOrganizations(this.organizations);
      this.db.writeCollaborations(this.collaborations);
      isWritten = true;
    }
    catch (Exception e) {
      e.printStackTrace();
      isWritten = false;
    }
    return isWritten;
  }

  /**
   * Gets the single instance of MyIsernModel object.
   * 
   * @return A MyIsernModel.
   */
  public static synchronized MyIsernModel getInstance() {
    return MyIsernModel.theInstance;
  }

  /**
   * Makes an unique ID using the name.
   * 
   * @param name The name that is used to make the unique ID.
   * @return The unique ID.
   */
  public synchronized String makeId(String name) {
    String id = name.replace(' ', '_');
    return id;
  }
  
  /**
   * Gets all the researchers as a map
   * 
   * @return researchers map
   */
  public synchronized Map<String, Researcher> getResearchersMap() {
    Map<String, Researcher> map = new HashMap<String, Researcher>();
    String id = null;
    for (Researcher tempResearcher : this.researchers.getResearcher()) {
      id = this.makeId(tempResearcher.getName());
      map.put(id, tempResearcher);
    }
    return map;
  }

  /**
   * Gets all the organizations as a map
   * 
   * @return organizations map
   */
  public synchronized Map<String, Organization> getOrganizationsMap() {
    Map<String, Organization> map = new HashMap<String, Organization>();
    String id = null;
    for (Organization tempOrg : this.organizations.getOrganization()) {
      id = this.makeId(tempOrg.getName());
      map.put(id, tempOrg);
    }
    return map;
  }

  /**
   * Get all the collaborations as a map
   * 
   * @return collaborations map
   */
  public synchronized Map<String, Collaboration> getCollaborationsMap() {
    Map<String, Collaboration> map = new HashMap<String, Collaboration>();
    String id = null;
    for (Collaboration tempCollab : this.collaborations.getCollaboration()) {
      id = this.makeId(tempCollab.getName());
      map.put(id, tempCollab);
    }
    return map;
  }

  /**
   * Gets a single researcher
   * 
   * @param name The name of the researcher
   * @return a single researcher
   */
  public synchronized Researcher getSingleResearcher(String name) {
    return this.getResearchersMap().get(this.makeId(name));
  }

  /**
   * Gets a single organization
   * 
   * @param name The name of the organization
   * @return a single organization
   */
  public synchronized Organization getSingleOrganization(String name) {
    return this.getOrganizationsMap().get(this.makeId(name));
  }

  /**
   * Gets a single collaboration
   * 
   * @param name The name of the collaboration
   * @return a single collaboration
   */
  public synchronized Collaboration getSingleCollaboration(String name) {
    return this.getCollaborationsMap().get(this.makeId(name));
  }

  /**
   * Gets all the researchers as a list
   * 
   * @return A list containing all the researchers.
   */
  public synchronized List<Researcher> getResearchersList() {
    return this.researchers.getResearcher();
  }

  /**
   * Gets all the organizations as a list
   * 
   * @return A list containing all the organizations.
   */
  public synchronized List<Organization> getOrganizationsList() {
    return this.organizations.getOrganization();
  }

  /**
   * Gets all the collaborations as a list
   * 
   * @return A list containing all the collaborations.
   */
  public synchronized List<Collaboration> getCollaborationsList() {
    return this.collaborations.getCollaboration();
  }
  
  /**
   * Adds or edits a researcher.
   * 
   * @param researcher The researcher to be added or edited.
   * @return The Researchers object after the adding or editing.
   */
  public synchronized Researchers addOrEditResearcher(Researcher researcher) {
    // get the unique ID
    String id = this.makeId(researcher.getName());
    // if the researcher already exists
    if (this.getResearchersMap().containsKey(id)) {
      Researcher tempResearcher = this.getResearchersMap().get(id);
      // construct a new Researchers object without the old researcher
      int index = this.getResearchersList().indexOf(tempResearcher);
      List<Researcher> list = this.getResearchersList();
      list.set(index, researcher);
      Researchers researchers = new Researchers();
      researchers.getResearcher().addAll(list);
      this.researchers = researchers;
      // update database
      this.db.updateResearcher(researcher);
    }
    // if the researcher is new
    else {
      this.researchers.getResearcher().add(researcher);
      // update database
      this.db.insertResearcher(researcher);
    }
    
    return this.researchers;
  }

  /**
   * Adds or edits an organization.
   * 
   * @param organization The organization to be added or edited.
   * @return The Organizations object after the adding or editing.
   */
  public synchronized Organizations addOrEditOrganization(Organization organization) {
    // get the unique ID
    String id = this.makeId(organization.getName());
    // if the organization already exists
    if (this.getOrganizationsMap().containsKey(id)) {
      Organization tempOrganization = this.getOrganizationsMap().get(id);
      // construct a new Organizations object without the old organization
      int index = this.getOrganizationsList().indexOf(tempOrganization);
      List<Organization> list = this.getOrganizationsList();
      list.set(index, organization);
      Organizations organizations = new Organizations();
      organizations.getOrganization().addAll(list);
      this.organizations = organizations;
      // update database
      this.db.updateOrganization(organization);
    }
    // if the organization is new
    else {
      this.organizations.getOrganization().add(organization);
      // update database
      this.db.insertOrganization(organization);
    }

    return this.organizations;
  }

  /**
   * Adds or edits a researcher.
   * 
   * @param collaboration The collaboration to be added or edited.
   * @return The Collaborations object.
   */
  public synchronized Collaborations addOrEditCollaboration(Collaboration collaboration) {
    // get the unique ID
    String id = this.makeId(collaboration.getName());
    // if the collaboration already exists
    if (this.getCollaborationsMap().containsKey(id)) {
      Collaboration tempCollaboration = this.getCollaborationsMap().get(id);
      // construct a new Collaborations object without the old collaboration
      int index = this.getCollaborationsList().indexOf(tempCollaboration);
      List<Collaboration> list = this.getCollaborationsList();
      list.set(index, collaboration);
      Collaborations collaborations = new Collaborations();
      collaborations.getCollaboration().addAll(list);
      this.collaborations = collaborations;
      // update database
      this.db.updateCollaboration(collaboration);
    }
    // if the collaboration is new
    else {
      this.collaborations.getCollaboration().add(collaboration);
      // update database
      this.db.insertCollaboration(collaboration);
    }
    
    return this.collaborations;
  }
  
  /**
   * Removes a researcher.
   * 
   * @param name The name of the researcher to be removed.
   * @return true if the researcher is deleted, false otherwise.
   */
  public synchronized boolean removeResearcher(String name) {
    // find the researcher to remove
    Researcher researcher = this.getSingleResearcher(name);
    // try to remove it
    if (this.researchers.getResearcher().remove(researcher)) {
      db.removeResearcher(researcher);
      return true;
    }
    return false;
  }
  
  /**
   * Removes an organization.
   * 
   * @param name The name of the organization to be removed.
   * @return true if the organization is deleted, false otherwise.
   */
  public synchronized boolean removeOrganization(String name) {
    // find the organization to remove
    Organization organization = this.getSingleOrganization(name);
    // try to remove it
    if (this.organizations.getOrganization().remove(organization)) {
      db.removeOrganization(organization);
      return true;
    }
    return false;
  }
  
  /**
   * Removes a collaboration.
   * 
   * @param name The name of the collaboration to be removed.
   * @return true if the collaboration is deleted, false otherwise.
   */
  public synchronized boolean removeCollaboration(String name) {
    // find the collaboration to remove
    Collaboration collaboration = this.getSingleCollaboration(name);
    // try to remove it
    if (this.collaborations.getCollaboration().remove(collaboration)) {
      db.removeCollaboration(collaboration);
      return true;
    }
    return false;
  }
  
  /**
   * Sorts the researchers by name.
   */
  public synchronized void sortResearchers() {
    List<Researcher> rNames = this.researchers.getResearcher();
    
    List<String> names = new ArrayList<String>();
    for (Researcher res : rNames) {
      names.add(res.getName());
    }
    
    Collections.sort(names);
    List<Researcher> sortedResearchers = new ArrayList<Researcher>();
    for (String name : names) {
      for (Researcher res : rNames) {
        if (name.equals(res.getName())) {
          sortedResearchers.add(res);
        }
      }
    }
    this.researchers.getResearcher().clear();
    this.researchers.getResearcher().addAll(sortedResearchers);
  }
  
  /**
   * Sorts the organizations by name.
   */
  public synchronized void sortOrganizations() {
    List<Organization> oNames = this.organizations.getOrganization();
    
    List<String> names = new ArrayList<String>();
    for (Organization org : oNames) {
      names.add(org.getName());
    }
    
    Collections.sort(names);
    List<Organization> sortedOrganizations = new ArrayList<Organization>();
    for (String name : names) {
      for (Organization org : oNames) {
        if (name.equals(org.getName())) {
          sortedOrganizations.add(org);
        }
      }
    }
    this.organizations.getOrganization().clear();
    this.organizations.getOrganization().addAll(sortedOrganizations);
  }
  
  /**
   * Sorts the collaborations by name.
   */
  public synchronized void sortCollaborations() {
    List<Collaboration> cNames = this.collaborations.getCollaboration();
    
    List<String> names = new ArrayList<String>();
    for (Collaboration collab : cNames) {
      names.add(collab.getName());
    }
    
    Collections.sort(names);
    List<Collaboration> sortedCollaborations = new ArrayList<Collaboration>();
    for (String name : names ) {
      for (Collaboration collab : cNames) {
        if (name.equals(collab.getName())) {
          sortedCollaborations.add(collab);
        }
      }
    }
    this.collaborations.getCollaboration().clear();
    this.collaborations.getCollaboration().addAll(sortedCollaborations);
  }
  
  /**
   * Creates and returns the list of researcher names.
   * 
   * @return The list of researcher names.
   */
  public synchronized List<String> getResearcherNames() {
    List<Researcher> researcherList = this.getResearchersList();
    List<String> researcherNamesList = new ArrayList<String>();
    for (Researcher res : researcherList) {
      researcherNamesList.add(res.getName());
    }
    Collections.sort(researcherNamesList);
    
    return researcherNamesList;
  }
    
  /**
   * Returns the string representation that will be used to generate the javascript array.
   * 
   * @return The string representation that will be used to generate the javascript array. 
   */
  public synchronized String getResearcherNamesList() {
    List<String> list = getResearcherNames();
    String delimiter = "|";
    StringBuffer names = new StringBuffer();
    for (String name : list) {
      names.append(name);
      names.append(delimiter);
    }

    return names.substring(0, (names.length() - 1));
  }
  
  /**
   * Creates and returns the list of organization names.
   * 
   * @return The list of organization names.
   */
  public synchronized List<String> getOrganizationNames() {
    List<Organization> organizationList = getOrganizationsList();
    List<String> organizationNamesList = new ArrayList<String>();
    for (Organization org : organizationList) {
      organizationNamesList.add(org.getName());
    }
    Collections.sort(organizationNamesList);
    
    return organizationNamesList;
  }
    
  /**
   * Returns the string representation that will be used to generate the javascript array.
   * 
   * @return The string representation that will be used to generate the javascript array. 
   */
  public synchronized String getOrganizationNamesList() {
    List<String> list = getOrganizationNames();
    String delimiter = "|";
    StringBuffer names = new StringBuffer();
    for (String name : list) {
      names.append(name);
      names.append(delimiter);
    }

    return names.substring(0, (names.length() - 1));
  }
  
  /**
   * Creates and returns the list of collaboration names.
   * 
   * @return The list of collaboration names.
   */
  public synchronized List<String> getCollaborationNames() {
    List<Collaboration> collaborationList = getCollaborationsList();
    List<String> collaborationNamesList = new ArrayList<String>();
    for (Collaboration collab : collaborationList) {
      collaborationNamesList.add(collab.getName());
    }
    Collections.sort(collaborationNamesList);
    
    return collaborationNamesList;
  }
  
  /**
   * Returns the string representation that will be used to generate the javascript array.
   * 
   * @return The string representation that will be used to generate the javascript array. 
   */
  public synchronized String getCollaborationNamesList() {
    List<String> list = getCollaborationNames();
    String delimiter = "|";
    StringBuffer names = new StringBuffer();
    for (String name : list) {
      names.append(name);
      names.append(delimiter);
    }

    return names.substring(0, (names.length() - 1));
  }
 
  /**
   * Import the XML researchers data to the Researchers object.
   * 
   * @param researchersFile A String representation of the researchers XML file.
   * @return true if the import succeeds, false otherwise.
   */
  public synchronized boolean importResearchersXml(String researchersFile) {
    boolean isImported = true;
    // try to load in the researchers XML file
    try {
      this.researchersJaxbContext = JAXBContext.newInstance("edu.hawaii.myisern.researchers.jaxb");
      File curFile = new File(researchersFile);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/researchers.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.researchersJaxbContext.createUnmarshaller();
      System.out.println("Importing researchers data from " + curFile);
      this.researchers = (Researchers) unmarshaller.unmarshal(curFile);
    }
    catch (Exception e) {
      isImported = false;
      e.printStackTrace();
    }
    // update database tables
    this.db.writeResearchers(this.researchers);
    return isImported;
  }
  
  /**
   * Import the XML organizations data to the Organizations object.
   * 
   * @param organizationsFile A String representation of the organizations XML file.
   * @return true if the import succeeds, false otherwise.
   */
  public synchronized boolean importOrganizationsXml(String organizationsFile) {
    boolean isImported = true;
    // try to load in the Organizations XML file
    try {
      this.organizationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.organizations.jaxb");
      File curFile = new File(organizationsFile);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/organizations.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.organizationsJaxbContext.createUnmarshaller();
      System.out.println("Importing organizations data from " + curFile);
      this.organizations = (Organizations) unmarshaller.unmarshal(curFile);
    }
    catch (Exception e) {
      isImported = false;
      e.printStackTrace();
    }    
    // update database tables
    this.db.writeOrganizations(this.organizations);
    return isImported;
  }
  
  /**
   * Import the XML collaborations data to the Collaborations object.
   * 
   * @param collaborationsFile A String representation of the collaborations XML file.
   * @return true if the import succeeds, false otherwise.
   */
  public synchronized boolean importCollaborationsXml(String collaborationsFile) {
    boolean isImported = true;
    // try to load in the Collaborations XML file
    try {
      this.collaborationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.collaborations.jaxb");
      File curFile = new File(collaborationsFile);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/collaborations.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.collaborationsJaxbContext.createUnmarshaller();
      System.out.println("Importing collaborations data from " + curFile);
      this.collaborations = (Collaborations) unmarshaller.unmarshal(curFile);
    }
    catch (Exception e) {
      isImported = false;
      e.printStackTrace();
    }
    // update database tables
    this.db.writeCollaborations(this.collaborations);
    return isImported;
  }
    
  /**
   * Export all the data by writing to the XML files.
   * 
   * @param researchersFile A String representation of the researchers XML file.
   * @param organizationsFile A String representation of the organizations XML file.
   * @param collaborationsFile A String representation of the collaborations XML file.
   * @return true if the export succeeds, false otherwise.
   */  
  public synchronized boolean exportXml(String researchersFile,
      String organizationsFile, String collaborationsFile) {
    boolean isExported = true;
    // save all changes for researchers
    try {
      JAXBContext researchersJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.researchers.jaxb");
      Marshaller marshaller = researchersJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(researchersFile);
      marshaller.marshal(this.researchers, os);
      System.out.println("Exporting researchers data to " + researchersFile);
      os.close();
    }
    catch (Exception e) {
      isExported = false;
      e.printStackTrace();
    }
    try {
      // save all changes for organizations
      JAXBContext organizationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.organizations.jaxb");
      Marshaller marshaller = organizationsJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(organizationsFile);
      marshaller.marshal(this.organizations, os);
      System.out.println("Exporting organizations data to " + organizationsFile);
      os.close();
    }
    catch (Exception e) {
      isExported = false;
      e.printStackTrace();
    }
    try {
      // save all changes for collaborations
      JAXBContext collaborationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.collaborations.jaxb");
      Marshaller marshaller = collaborationsJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(collaborationsFile);
      marshaller.marshal(this.collaborations, os);
      System.out.println("Exporting collaborations data to " + collaborationsFile);
      os.close();
    }
    catch (Exception e) {
      isExported = false;
      e.printStackTrace();
    }
    return isExported;
  }

  /**
   * Helper method to copy the sample XML files.
   * This method is inspired from http://www.rgagnon.com/javadetails/java-0064.html
   * 
   * @param in Source file.
   * @param out Destination file.
   * @throws Exception If error occurs.
   */
  protected synchronized void copyFile(File in, File out) throws Exception {
    FileChannel inChannel = null;
    FileChannel outChannel = null;
    try {
      inChannel = new FileInputStream(in).getChannel();
      outChannel = new FileOutputStream(out).getChannel(); 
      inChannel.transferTo(0, inChannel.size(), outChannel);
      System.out.println("Copying from " + in + " to " + out);
    } 
    catch (Exception e) {
      e.printStackTrace();
    }
    finally {
      if (inChannel != null) { 
        inChannel.close();
      }
      if (outChannel != null) { 
        outChannel.close();
      }
    }
  }
  
  /*
  public synchronized boolean shutDown() {
    return this.db.shutDown();
  }*/
}